/*
 * Copyright (c) 2011 Imre Fazekas.
 * All rights reserved.
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 * Redistributions of source code must retain the above copyright notice, this
 * list of conditions and the following disclaimer.
 *
 * Redistributions in binary form must reproduce the above copyright notice,
 * this list of conditions and the following disclaimer in the documentation
 * and/or other materials provided with the distribution.
 * Neither the name of the Brillien nor the names of its
 * terms and concepts may be used to endorse or promote products derived from this
 * software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 */

package com.vii.streamline.web;

import com.vii.streamline.json.JsonRpcRequest;
import com.vii.streamline.json.JsonRpcResponse;
import com.vii.streamline.services.IOServices;
import com.vii.streamline.services.ThreadServices;
import com.vii.streamline.services.error.StreamLineException;
import com.vii.streamline.services.json.JsonServices;
import com.vii.streamline.structures.collections.InnerMap;
import org.apache.http.*;
import org.apache.http.client.HttpRequestRetryHandler;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.utils.URIUtils;
import org.apache.http.client.utils.URLEncodedUtils;
import org.apache.http.conn.scheme.PlainSocketFactory;
import org.apache.http.conn.scheme.Scheme;
import org.apache.http.conn.scheme.SchemeRegistry;
import org.apache.http.conn.ssl.SSLSocketFactory;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.impl.conn.ProxySelectorRoutePlanner;
import org.apache.http.impl.conn.tsccm.ThreadSafeClientConnManager;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.params.BasicHttpParams;
import org.apache.http.params.CoreProtocolPNames;
import org.apache.http.params.HttpParams;
import org.apache.http.protocol.ExecutionContext;
import org.apache.http.protocol.HttpContext;
import org.apache.http.util.EntityUtils;

import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLHandshakeException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.*;
import java.net.ProxySelector;
import java.net.URI;
import java.security.NoSuchAlgorithmException;
import java.util.*;

/**
 * Utility class to advance jsonrpc-related activities.
 */
public class HttpServices {


    public static int       MAX_CONNECTIONS         = 1000;
    public static int       REPLY_COUNT             = 3;
    public static boolean   HTTPS_SUPPORT           = true;

    public static int       READ_BUFFER             = 4096;

    public static String    HTTP_ELEMENT_CHARSET    = "UTF-8";
    public static String    HTTP_CONTENT_CHARSET    = "UTF-8";
    public static String    USER_AGENT              = "Apache HttpClient";

    /**
     * Thread-safe connection manager for the HttpClient instance
     */
    public static ThreadSafeClientConnManager cm;

    /**
     * Http client provided by the Apache's commons http library
     */
    public static DefaultHttpClient httpclient;

    /**
     * Initiates the jsonrpc client
     */
    public static void initWebClient() {
        initWebClient(REPLY_COUNT, MAX_CONNECTIONS);
    }

    /**
     * Initiates the jsonrpc client
     */
    public static void initWebClient(final long replyCount, int maxConnections) {
        if (httpclient == null) {
            Scheme http = new Scheme("http", 80, PlainSocketFactory.getSocketFactory());
            SchemeRegistry sr = new SchemeRegistry();

            sr.register(http);
            if( HTTPS_SUPPORT )
                try{
                    SSLSocketFactory sf = new SSLSocketFactory(
                            SSLContext.getDefault(),
                            SSLSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER);
                    Scheme https = new Scheme("https", 443, sf);

                    sr.register(https);
                } catch (NoSuchAlgorithmException e) {
                    e.printStackTrace();
                }

            cm = new ThreadSafeClientConnManager( sr );
            cm.setMaxTotal(maxConnections);

            HttpParams params = new BasicHttpParams();
            params.setParameter( CoreProtocolPNames.HTTP_ELEMENT_CHARSET, HTTP_ELEMENT_CHARSET );
            params.setParameter( CoreProtocolPNames.HTTP_CONTENT_CHARSET, HTTP_CONTENT_CHARSET );
            params.setParameter( CoreProtocolPNames.USER_AGENT, USER_AGENT );

            httpclient = new DefaultHttpClient(cm, params);

            HttpRequestRetryHandler myRetryHandler = new HttpRequestRetryHandler() {
                public boolean retryRequest(
                        IOException exception,
                        int executionCount,
                        HttpContext context) {
                    if (executionCount >= replyCount) {
                        return false;
                    }
                    if (exception instanceof NoHttpResponseException) {
                        return true;
                    }
                    if (exception instanceof SSLHandshakeException) {
                        return false;
                    }
                    HttpRequest request = (HttpRequest) context.getAttribute(
                            ExecutionContext.HTTP_REQUEST);
                    boolean idempotent = !(request instanceof HttpEntityEnclosingRequest);
                    if (idempotent) {
                        return true;
                    }
                    return false;
                }
            };

            httpclient.setHttpRequestRetryHandler(myRetryHandler);

            ProxySelectorRoutePlanner routePlanner = new ProxySelectorRoutePlanner(
            httpclient.getConnectionManager().getSchemeRegistry(), ProxySelector.getDefault());
            httpclient.setRoutePlanner(routePlanner);

            ThreadServices.getStpe().submit(new IdleConnectionMonitorThread(cm));
        }
    }

    public static void shutdownWebClient() {
        httpclient.getConnectionManager().shutdown();
    }

    /**
     * Retrieves the parameters of the request
     */
    public final static HashMap<String, Object> getRequestParameters(HttpServletRequest httpServletRequest){
        HashMap<String, Object> parameters = new HashMap<String, Object>();

        Enumeration<String> e = httpServletRequest.getParameterNames();
        while( e.hasMoreElements()  ){
            String param = e.nextElement();
            parameters.put( param, httpServletRequest.getParameter(param) );
        }

        return parameters;
    }

    public final static byte[] getRequestBinaryBody(HttpServletRequest httpServletRequest, long maxSizeToRead) throws StreamLineException {
        long totalSumm = 0;
        try {
            InputStream inputStream = httpServletRequest.getInputStream();
            ByteArrayOutputStream bout = new ByteArrayOutputStream( );
            if (inputStream != null) {
                byte[] byteBuffer = new byte[ READ_BUFFER ];
                int bytesRead = -1;
                while ((bytesRead = inputStream.read(byteBuffer)) > 0 && totalSumm<maxSizeToRead ) {
                    bout.write( byteBuffer, 0, bytesRead );
                    totalSumm += bytesRead;
                }
            }
            bout.close();
            return bout.toByteArray();
        } catch (Exception e) {
            throw new StreamLineException(e.getMessage(), e);
        }
    }

    public final static byte[] getRequestBinaryBody(HttpServletRequest httpServletRequest) throws StreamLineException {
        try {
            InputStream inputStream = httpServletRequest.getInputStream();
            ByteArrayOutputStream bout = new ByteArrayOutputStream( );
            if (inputStream != null) {
                byte[] byteBuffer = new byte[ READ_BUFFER ];
                int bytesRead = -1;
                while ((bytesRead = inputStream.read(byteBuffer)) > 0) {
                    bout.write( byteBuffer, 0, bytesRead );
                }
            }
            bout.close();
            return bout.toByteArray();
        } catch (Exception e) {
            throw new StreamLineException(e.getMessage(), e);
        }
    }
    /**
     * Gets the body of a request as a String
     */
    public final static String getRequestBody(HttpServletRequest httpServletRequest) throws StreamLineException {
        try {
            StringBuilder stringBuilder = new StringBuilder();
            BufferedReader bufferedReader = null;
            try {
                InputStream inputStream = httpServletRequest.getInputStream();
                if (inputStream != null) {
                    bufferedReader = new BufferedReader(new InputStreamReader(
                            inputStream));
                    char[] charBuffer = new char[ READ_BUFFER ];
                    int bytesRead = -1;
                    while ((bytesRead = bufferedReader.read(charBuffer)) > 0) {
                        stringBuilder.append(charBuffer, 0, bytesRead);
                    }
                } else {
                    stringBuilder.append("");
                }
            } catch (IOException ex) {
                throw ex;
            } finally {
                if (bufferedReader != null) {
                    try {
                        bufferedReader.close();
                    } catch (IOException ex) {
                        throw ex;
                    }
                }
            }

            return stringBuilder.toString();
        } catch (Exception e) {
            throw new StreamLineException(e.getMessage(), e);
        }
    }

    /**
     * Send a response message from a Servlet to a client
     */
    public static void sendResponseMessage(HttpServletResponse httpServletResponse, Object o) throws StreamLineException {
        try {
            httpServletResponse.setContentType( "text/plain; charset=" + HTTP_CONTENT_CHARSET );
            httpServletResponse.getWriter().print( o );
        } catch (Exception e) {
            throw new StreamLineException(e.getMessage(), e);
        }
    }


    /**
     * Send a JSON message from a Servlet to a client
     */
    public static void sendJSONMessage(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, boolean compress) throws StreamLineException {
        try {
            httpServletResponse.setContentType(compress ? "application/octet-stream" : "application/json; charset=" + HTTP_CONTENT_CHARSET);
            if (compress) {
                httpServletResponse.getOutputStream().write(IOServices.compress(JsonServices.toJSON(o)));
            } else {
                httpServletResponse.getWriter().print(JsonServices.toJSON(o));
            }
        } catch (Exception e) {
            throw new StreamLineException(e.getMessage(), e);
        }
    }

    /**
     * Send a JSON message from a Servlet to a client
     */
    public static void sendJSONMessage(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o) throws StreamLineException {
        sendJSONMessage(httpServletRequest, httpServletResponse, o, false);
    }

    /**
     * Gets the body of the post received by a Servlet
     */
    public static String getJSONMessage(HttpServletRequest httpServletRequest) throws StreamLineException {
        return getRequestBody(httpServletRequest);
    }


    /**
     * Sends a JSON-RPC 2.0 message to the given URL via HTTP-POST
     */
    public static <T> T sendJsonRpcMessage(String url, String serviceName, Map<String, Object> params) throws StreamLineException {
        return HttpServices.<T>sendJsonRpcMessage(url, null, serviceName, params);
    }

    /**
     * Sends a JSON-RPC 2.0 message to the given URL via HTTP-POST
     */
    public static <T> T sendJsonRpcMessage(String url, String serviceName, Map<String, Object> params, String typeHint, Class resType) throws StreamLineException {
        return HttpServices.<T>sendJsonRpcMessage(url, null, serviceName, params, typeHint, resType);
    }

    /**
     * Sends a JSON-RPC 2.0 message to the given URL via HTTP-POST
     */
    public static <T> T sendJsonRpcMessage(String url, String serviceName, Map<String, Object> params, Map<String, Class> typeHints) throws StreamLineException {
        return HttpServices.<T>sendJsonRpcMessage(url, null, serviceName, params, typeHints);
    }

    /**
     * Sends a JSON-RPC 2.0 message to the given URL via HTTP-POST
     */
    public static <T> T sendJsonRpcMessage(String url, String sessionId, String serviceName, Map<String, Object> params) throws StreamLineException {
        return HttpServices.<T>sendJsonRpcMessage(url, sessionId, serviceName, params, null, null);
    }

    /**
     * Sends a JSON-RPC 2.0 message to the given URL via HTTP-POST
     */
    public static <T> T sendJsonRpcMessage(String url, String sessionId, String serviceName, Map<String, Object> params, String typeHint, Class resType) throws StreamLineException {
        JsonRpcRequest<Map, String> jsonRequest = JsonRpcRequest.createMapRequest(serviceName);
        if (params != null) {
            for (String s : params.keySet()) {
                jsonRequest.addParameter(s, params.get(s));
            }
        }
        jsonRequest.setSessionId(sessionId);

        String message = JsonServices.toJSON(jsonRequest);

        String resp = sendMessage(url, message);

        JsonRpcResponse response = typeHint == null
                ? JsonServices.parseJSON(resp, JsonRpcResponse.class)
                : JsonServices.parseJSON(resp, JsonRpcResponse.class, typeHint, resType);

        if( response.getError() != null )
            throw new StreamLineException( response.getError().getCode(), response.getError().getMessage(), response.getError().getData() );

        return (T) response.getResult();
    }

    /**
     * Sends a JSON-RPC 2.0 message to the given URL via HTTP-POST
     */
    public static <T> T sendJsonRpcMessage(String url, String sessionId, String serviceName, Map<String, Object> params, Map<String, Class> typeHints) throws StreamLineException {
        JsonRpcRequest<Map, String> jsonRequest = JsonRpcRequest.createMapRequest(serviceName);
        if (params != null) {
            for (String s : params.keySet()) {
                jsonRequest.addParameter(s, params.get(s));
            }
        }
        jsonRequest.setSessionId(sessionId);

        String message = JsonServices.toJSON(jsonRequest);

        String resp = sendMessage(url, message);

        JsonRpcResponse response = typeHints == null
                ? JsonServices.parseJSON(resp, JsonRpcResponse.class)
                : JsonServices.parseJSON(resp, JsonRpcResponse.class, typeHints);

        if( response.getError() != null )
            throw new StreamLineException( response.getError().getCode(), response.getError().getMessage(), response.getError().getData() );


        return (T) response.getResult();
    }

    /**
     * Sends a JSON-RPC 2.0 message to the given URL via HTTP-POST
     */
    public static <T> T sendJsonRpcMessage(String url, String serviceName, List<Object> params) throws StreamLineException {
        return HttpServices.<T>sendJsonRpcMessage(url, null, serviceName, params);
    }

    /**
     * Sends a JSON-RPC 2.0 message to the given URL via HTTP-POST
     */
    public static <T> T sendJsonRpcMessage(String url, String serviceName, List<Object> params, String typeHint, Class resType) throws StreamLineException {
        return HttpServices.<T>sendJsonRpcMessage(url, null, serviceName, params, typeHint, resType);
    }

    /**
     * Sends a JSON-RPC 2.0 message to the given URL via HTTP-POST
     */
    public static <T> T sendJsonRpcMessage(String url, String serviceName, List<Object> params, Map<String, Class> typeHints) throws StreamLineException {
        return HttpServices.<T>sendJsonRpcMessage(url, null, serviceName, params, typeHints);
    }

    /**
     * Sends a JSON-RPC 2.0 message to the given URL via HTTP-POST
     */
    public static <T> T sendJsonRpcMessage(String url, String sessionId, String serviceName, List<Object> params) throws StreamLineException {
        return HttpServices.<T>sendJsonRpcMessage(url, sessionId, serviceName, params, null, null);
    }

    /**
     * Sends a JSON-RPC 2.0 message to the given URL via HTTP-POST
     */
    public static <T> T sendJsonRpcMessage(String url, String sessionId, String serviceName, List<Object> params, String typeHint, Class resType) throws StreamLineException {
        JsonRpcRequest<List, String> jsonRequest = JsonRpcRequest.createListRequest(serviceName, params);
        jsonRequest.setSessionId(sessionId);

        String message = JsonServices.toJSON(jsonRequest);

        String resp = sendMessage(url, message);

        JsonRpcResponse response = typeHint == null
                ? JsonServices.parseJSON(resp, JsonRpcResponse.class)
                : JsonServices.parseJSON(resp, JsonRpcResponse.class, typeHint, resType);

        if( response.getError() != null )
            throw new StreamLineException( response.getError().getCode(), response.getError().getMessage(), response.getError().getData() );

        return (T) response.getResult();
    }

    /**
     * Sends a JSON-RPC 2.0 message to the given URL via HTTP-POST
     */
    public static <T> T sendJsonRpcMessage(String url, String sessionId, String serviceName, List<Object> params, Map<String, Class> typeHints) throws StreamLineException {
        JsonRpcRequest<List, String> jsonRequest = JsonRpcRequest.createListRequest(serviceName, params);
        jsonRequest.setSessionId(sessionId);

        String message = JsonServices.toJSON(jsonRequest);

        String resp = sendMessage(url, message);

        JsonRpcResponse response = typeHints == null
                ? JsonServices.parseJSON(resp, JsonRpcResponse.class)
                : JsonServices.parseJSON(resp, JsonRpcResponse.class, typeHints);

        if( response.getError() != null )
            throw new StreamLineException( response.getError().getCode(), response.getError().getMessage(), response.getError().getData() );

        return (T) response.getResult();
    }

    /**
     * Sends a message to the given URL via HTTP-POST
     */
    public static String sendMessage(String url, String message) throws StreamLineException {
        initWebClient();

        HttpPost post = new HttpPost(url);

        try {
            StringEntity myEntity = new StringEntity(message, HTTP_CONTENT_CHARSET);
            post.setEntity(myEntity);

            HttpResponse response = httpclient.execute(post);

            HttpEntity entity = response.getEntity();
            return entity == null ? "" : EntityUtils.toString(entity);
        } catch (Exception e) {
            throw new StreamLineException(e);
        }
    }

    /**
     * Sends a message to the given URL via HTTP-POST and retrieves the content as byte[]
     */
    public static byte[] download(String url, String message) throws StreamLineException {
        initWebClient();

        HttpPost post = new HttpPost(url);

        try {
            StringEntity myEntity = new StringEntity(message, HTTP_CONTENT_CHARSET);
            post.setEntity(myEntity);

            HttpResponse response = httpclient.execute(post);

            HttpEntity entity = response.getEntity();
            return entity == null ? null : EntityUtils.toByteArray(entity);
        } catch (Exception e) {
            throw new StreamLineException(e);
        }
    }

    public static String sendGetMessage(String host, String path) throws StreamLineException {
        return sendGetMessage( "http", host, 80, path, null );
    }

    public static String sendGetMessage(String host, int port, String path) throws StreamLineException {
        return sendGetMessage( "http", host, port, path, null );
    }

    public static String sendGetMessage(String host, String path, Map<String, Object> params) throws StreamLineException {
        return sendGetMessage( "http", host, 80, path, params );
    }

    public static String sendGetMessage(String host, int port, String path, Map<String, Object> params) throws StreamLineException {
        return sendGetMessage( "http", host, port, path, params );
    }

    public static String sendGetMessage(String url) throws StreamLineException {
        initWebClient();

        HttpGet get = new HttpGet( url );

        try {
            HttpResponse response = httpclient.execute( get );

            HttpEntity entity = response.getEntity();
            return entity == null ? "" : EntityUtils.toString(entity);
        } catch (Exception e) {
            throw new StreamLineException(e);
        }
    }

    public static String sendGetMessage(String scheme, String host, int port, String path, Map<String, Object> params) throws StreamLineException {
        initWebClient();

        List<NameValuePair> qparams = new ArrayList<NameValuePair>();

        if( params != null ){
            for( String paramName : params.keySet() )
                qparams.add(new BasicNameValuePair( paramName, params.get(paramName).toString() ));
        }

        try {
            URI uri = URIUtils.createURI(scheme, host, port, path, URLEncodedUtils.format(qparams, HTTP_ELEMENT_CHARSET), null);

            HttpGet get = new HttpGet( uri );

            HttpResponse response = httpclient.execute( get );

            HttpEntity entity = response.getEntity();
            return entity == null ? "" : EntityUtils.toString(entity);
        } catch (Exception e) {
            throw new StreamLineException(e);
        }
    }

    public static String sendPostMessage(String scheme, String host, int port, String path, Map<String, Object> params) throws StreamLineException {
        return sendPostMessage(scheme, host, port, path, params, null);
    }
    public static String sendPostJsonMessage(String scheme, String host, int port, String path, Object message) throws StreamLineException {
        return sendPostMessage(scheme, host, port, path, new InnerMap<String, String>("Content-Type", "application/json"), null, message == null ? null : JsonServices.toJSON(message));
    }
    public static String sendPostJsonMessage(String scheme, String host, int port, String path, Map<String, Object> params, Object message) throws StreamLineException {
        return sendPostMessage(scheme, host, port, path, new InnerMap<String, String>("Content-Type", "application/json"), params, message == null ? null : JsonServices.toJSON(message));
    }
    public static String sendPostMessage(String scheme, String host, int port, String path, Map<String, Object> params, String message) throws StreamLineException {
        return sendPostMessage(scheme, host, port, path, null, params, message == null ? null : JsonServices.toJSON(message));
    }
    public static String sendPostMessage(String scheme, String host, int port, String path, Map<String, String> header, Map<String, Object> params, String message) throws StreamLineException {
        initWebClient();

        List<NameValuePair> qparams = new ArrayList<NameValuePair>();

        if( params != null ){
            for( String paramName : params.keySet() )
                qparams.add(new BasicNameValuePair( paramName, params.get(paramName).toString() ));
        }

        try {
            URI uri = URIUtils.createURI(scheme, host, port, path, URLEncodedUtils.format(qparams, HTTP_ELEMENT_CHARSET), null);

            HttpPost post = new HttpPost( uri );

            if( header != null ){
                for( String key : header.keySet() )
                    post.addHeader( key, header.get(key) );
            }

            if( message != null ){
                StringEntity myEntity = new StringEntity(message, HTTP_CONTENT_CHARSET);
                post.setEntity(myEntity);
            }

            HttpResponse response = httpclient.execute( post );

            HttpEntity entity = response.getEntity();
            return entity == null ? "" : EntityUtils.toString(entity);
        } catch (Exception e) {
            throw new StreamLineException(e);
        }
    }

}
